From dc424280a10b43b3855ff3ee247685d30e415a84 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 11 May 2010 15:00:07 -0400 Subject: [PATCH] Add missing magnifier files from the last commit Also, this patch differs from the one in Bugzilla in that we start/stop tracking the mouse depending on whether the magnifier is active. --- js/ui/magnifier.js | 1484 ++++++++++++++++++++++++++++++++++++++++ js/ui/magnifierDBus.js | 375 ++++++++++ 2 files changed, 1859 insertions(+) create mode 100644 js/ui/magnifier.js create mode 100644 js/ui/magnifierDBus.js diff --git a/js/ui/magnifier.js b/js/ui/magnifier.js new file mode 100644 index 000000000..75e2f5ab5 --- /dev/null +++ b/js/ui/magnifier.js @@ -0,0 +1,1484 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const DBus = imports.dbus; +const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; +const Clutter = imports.gi.Clutter; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Main = imports.ui.main; +const MagnifierDBus = imports.ui.magnifierDBus; + +const MouseTrackingMode = { + NONE: 0, + CENTERED: 1, + PUSH: 2, + PROPORTIONAL: 3 +}; + +const ScreenPosition = { + NONE: 0, + FULL_SCREEN: 1, + TOP_HALF: 2, + BOTTOM_HALF: 3, + LEFT_HALF: 4, + RIGHT_HALF: 5 +}; + +// Default settings +const DEFAULT_X_MAGFACTOR = 2; +const DEFAULT_Y_MAGFACTOR = 2; +const DEFAULT_MOUSE_POLL_FREQUENCY = 50; +const DEFAULT_LENS_MODE = false; +const DEFAULT_SCREEN_POSITION = ScreenPosition.BOTTOM_HALF; +const DEFAULT_MOUSE_TRACKING_MODE = MouseTrackingMode.CENTERED; +const DEFAULT_CLAMP_SCROLLING_AT_EDGES = true; + +const DEFAULT_SHOW_CROSSHAIRS = false; +const DEFAULT_CROSSHAIRS_THICKNESS = 8; +const DEFAULT_CROSSHAIRS_OPACITY = 169; // 66% +const DEFAULT_CROSSHAIRS_LENGTH = 4096; +const DEFAULT_CROSSHAIRS_CLIP = false; +const DEFAULT_CROSSHAIRS_CLIP_SIZE = [100, 100]; +const DEFAULT_CROSSHAIRS_COLOR = new Clutter.Color(); +DEFAULT_CROSSHAIRS_COLOR.from_string("Red"); + +// GConf settings +const A11Y_MAG_PREFS_DIR = "/desktop/gnome/accessibility/magnifier"; +const SHOW_KEY = A11Y_MAG_PREFS_DIR + "/show_magnifier"; +const SCREEN_POSITION_KEY = A11Y_MAG_PREFS_DIR + "/screen_position"; +const MAG_FACTOR_KEY = A11Y_MAG_PREFS_DIR + "/mag_factor"; +const LENS_MODE_KEY = A11Y_MAG_PREFS_DIR + "/lens_mode"; +const CLAMP_MODE_KEY = A11Y_MAG_PREFS_DIR + "/scroll_at_edges"; +const MOUSE_TRACKING_KEY = A11Y_MAG_PREFS_DIR + "/mouse_tracking"; +const SHOW_CROSS_HAIRS_KEY = A11Y_MAG_PREFS_DIR + "/show_cross_hairs"; +const CROSS_HAIRS_THICKNESS_KEY = A11Y_MAG_PREFS_DIR + "/cross_hairs_thickness"; +const CROSS_HAIRS_COLOR_KEY = A11Y_MAG_PREFS_DIR + "/cross_hairs_color"; +const CROSS_HAIRS_OPACITY_KEY = A11Y_MAG_PREFS_DIR + "/cross_hairs_opacity"; +const CROSS_HAIRS_LENGTH_KEY = A11Y_MAG_PREFS_DIR + "/cross_hairs_length"; +const CROSS_HAIRS_CLIP_KEY = A11Y_MAG_PREFS_DIR + "/cross_hairs_clip"; + +let magDBusService = null; + +function Magnifier() { + this._init(); +} + +Magnifier.prototype = { + _init: function() { + // Magnifier is a manager of ZoomRegions. + this._zoomRegions = []; + + // Create small clutter tree for the magnified mouse. + let xfixesCursor = Shell.XFixesCursor.get_default(); + this._mouseSprite = new Clutter.Texture(); + xfixesCursor.update_texture_image(this._mouseSprite); + this._cursorRoot = new Clutter.Group(); + this._cursorRoot.add_actor(this._mouseSprite); + + // Create the first ZoomRegion and initialize it according to the + // magnification GConf settings. + let [objUnder, xMouse, yMouse, mask] = + Gdk.Screen.get_default().get_root_window().get_pointer(); + let aZoomRegion = new ZoomRegion(this, this._cursorRoot); + this._zoomRegions.push(aZoomRegion); + let showAtLaunch = this._gConfInit(aZoomRegion); + aZoomRegion.scrollContentsTo(xMouse, yMouse); + + xfixesCursor.connect('cursor-change', Lang.bind(this, this._updateMouseSprite)); + this._xfixesCursor = xfixesCursor; + + // Export to dbus. + magDBusService = new MagnifierDBus.ShellMagnifier(); + this.setActive(showAtLaunch); + }, + + /** + * showSystemCursor: + * Show the system mouse pointer. + */ + showSystemCursor: function() { + this._xfixesCursor.show(); + }, + + /** + * hideSystemCursor: + * Hide the system mouse pointer. + */ + hideSystemCursor: function() { + this._xfixesCursor.hide(); + }, + + /** + * setActive: + * Show/hide all the zoom regions. + * @activate: Boolean to activate or de-activate the magnifier. + */ + setActive: function(activate) { + this._zoomRegions.forEach (function(zoomRegion, index, array) { + zoomRegion.setActive(activate); + }); + + if (activate) + this.startTrackingMouse(); + else + this.stopTrackingMouse(); + + // Make sure system mouse pointer is shown when all zoom regions are + // invisible. + if (!activate) + this._xfixesCursor.show(); + }, + + /** + * isActive: + * @return Whether the magnifier is active (boolean). + */ + isActive: function() { + // Sufficient to check one ZoomRegion since Magnifier's active + // state applies to all of them. + if (this._zoomRegions.length == 0) + return false; + else + return this._zoomRegions[0].isActive(); + }, + + /** + * startTrackingMouse: + * Turn on mouse tracking, if not already doing so. + */ + startTrackingMouse: function() { + // initialize previous mouse coord to undefined. + let prevCoord = { x: NaN, y: NaN }; + if (!this._mouseTrackingId) + this._mouseTrackingId = Mainloop.timeout_add( + DEFAULT_MOUSE_POLL_FREQUENCY, + Lang.bind(this, this.scrollToMousePos, prevCoord) + ); + }, + + /** + * stopTrackingMouse: + * Turn off mouse tracking, if not already doing so. + */ + stopTrackingMouse: function() { + if (this._mouseTrackingId) + Mainloop.source_remove(this._mouseTrackingId); + + this._mouseTrackingId = null; + }, + + /** + * isTrackingMouse: + * Is the magnifier tracking the mouse currently? + */ + isTrackingMouse: function() { + return !!this._mouseTrackingId; + }, + + /** + * scrollToMousePos: + * Position all zoom regions' ROI relative to the current location of the + * system pointer. + * @prevCoord: The previous mouse coordinates. Used to stop scrolling if + * the new position is the same as the last one (optional). + * @return true. + */ + scrollToMousePos: function(prevCoord) { + let [objUnder, xMouse, yMouse, mask] = + Gdk.Screen.get_default().get_root_window().get_pointer(); + + if (!prevCoord || prevCoord.x != xMouse || prevCoord.y != yMouse) { + let sysMouseOverAny = false; + this._zoomRegions.forEach(function(zoomRegion, index, array) { + if (zoomRegion.scrollToMousePos()) + sysMouseOverAny = true; + }); + if (sysMouseOverAny) + this.hideSystemCursor(); + else + this.showSystemCursor(); + + if (prevCoord) { + prevCoord.x = xMouse; + prevCoord.y = yMouse; + } + } + return true; + }, + + /** + * createZoomRegion: + * Create a ZoomRegion instance with the given properties. + * @xMagFactor: The power to set horizontal magnification of the + * ZoomRegion. A value of 1.0 means no magnification. A + * value of 2.0 doubles the size. + * @yMagFactor: The power to set the vertical magnification of the + * ZoomRegion. + * @roi Object in the form { x, y, width, height } that + * defines the region to magnify. Given in unmagnified + * coordinates. + * @viewPort Object in the form { x, y, width, height } that defines + * the position of the ZoomRegion on screen. + * @return The newly created ZoomRegion. + */ + createZoomRegion: function(xMagFactor, yMagFactor, roi, viewPort) { + let zoomRegion = new ZoomRegion(this, this._cursorRoot); + zoomRegion.setMagFactor(xMagFactor, yMagFactor); + zoomRegion.setViewPort(viewPort); + zoomRegion.setROI(roi); + zoomRegion.addCrosshairs(this._crossHairs); + return zoomRegion; + }, + + /** + * addZoomRegion: + * Append the given ZoomRegion to the list of currently defined ZoomRegions + * for this Magnifier instance. + * @zoomRegion: The zoomRegion to add. + */ + addZoomRegion: function(zoomRegion) { + if(zoomRegion) { + this._zoomRegions.push(zoomRegion); + if (!this.isTrackingMouse()) + this.startTrackingMouse(); + } + }, + + /** + * getZoomRegions: + * Return a list of ZoomRegion's for this Magnifier. + * @return: The Magnifier's zoom region list (array). + */ + getZoomRegions: function() { + return this._zoomRegions; + }, + + /** + * clearAllZoomRegions: + * Remove all the zoom regions from this Magnfier's ZoomRegion list. + */ + clearAllZoomRegions: function() { + // First ZoomRegion is special since its magnified mouse and crosshairs + // are the original -- all the others are Clutter.Clone's. Deal with + // all but first zoom region. + for (let i = 1; i < this._zoomRegions.length; i++) { + this._zoomRegions[i].setActive(false); + this._zoomRegions[i].removeFromStage(); + } + this._zoomRegions[0].setActive(false); + + // Detach the (original) magnified mouse and cross hair for later reuse + // before removing ZoomRegion from the stage. + this._cursorRoot.get_parent().remove_actor(this._cursorRoot); + if (this._crossHairs) + this._crossHairs.removeFromParent(); + + this._zoomRegions[0].removeFromStage(); + this._zoomRegions.length = 0; + this.stopTrackingMouse(); + this.showSystemCursor(); + }, + + /** + * addCrosshairs: + * Add and show a cross hair centered on the magnified mouse. + * @thickness: The thickness of the vertical and horizontal lines of the + * crosshair. + * @color: The color of the crosshairs + * @opacity: The opacity. + * @length: The length of each hair. + * @clip: Whether the crosshairs intersection is clipped by the + * magnified mouse image. + */ + addCrosshairs: function(thickness, color, opacity, length, clip) { + if (!this._crossHairs) + this._crossHairs = new Crosshairs(); + + this.setCrosshairsThickness(thickness); + this.setCrosshairsColor(color); + this.setCrosshairsOpacity(opacity); + this.setCrosshairsLength(length); + this.setCrosshairsClip(clip); + + let theCrossHairs = this._crossHairs; + this._zoomRegions.forEach (function(zoomRegion, index, array) { + zoomRegion.addCrosshairs(theCrossHairs); + }); + }, + + /** + * setCrosshairsVisible: + * Show or hide the cross hair. + * @visible Flag that indicates show (true) or hide (false). + */ + setCrosshairsVisible: function(visible) { + if (visible) { + if (!this._crossHairs) + this.addCrosshairs(DEFAULT_CROSSHAIRS_THICKNESS, DEFAULT_CROSSHAIRS_COLOR, DEFAULT_CROSSHAIRS_OPACITY, DEFAULT_CROSSHAIRS_CLIP); + this._crossHairs.show(); + } + else { + if (this._crossHairs) + this._crossHairs.hide(); + } + }, + + /** + * setCrosshairsColor: + * Set the color of the crosshairs for all ZoomRegions. + * @color: The color as a string, e.g. "#ff0000ff" or "red". + */ + setCrosshairsColor: function(color) { + if (this._crossHairs) { + let clutterColor = new Clutter.Color(); + clutterColor.from_string(color); + this._crossHairs.setColor(clutterColor); + } + }, + + /** + * getCrosshairsColor: + * Get the color of the crosshairs. + * @return: The color as a string, e.g. "#0000ffff" for blue. + */ + getCrosshairsColor: function() { + if (this._crossHairs) { + let clutterColor = this._crossHairs.getColor(); + return clutterColor.to_string(); + } + else + return "#00000000"; + }, + + /** + * setCrosshairsThickness: + * Set the crosshairs thickness for all ZoomRegions. + * @thickness: The width of the vertical and horizontal lines of the + * crosshairs. + */ + setCrosshairsThickness: function(thickness) { + if (this._crossHairs) + this._crossHairs.setThickness(thickness); + }, + + /** + * getCrosshairsThickness: + * Get the crosshairs thickness. + * @return: The width of the vertical and horizontal lines of the + * crosshairs. + */ + getCrosshairsThickness: function() { + if (this._crossHairs) + return this._crossHairs.getThickness(); + else + return 0; + }, + + /** + * setCrosshairsOpacity: + * @opacity: Value between 0 (transparent) and 255 (fully opaque). + */ + setCrosshairsOpacity: function(opacity) { + if (this._crossHairs) + this._crossHairs.setOpacity(opacity); + }, + + /** + * getCrosshairsOpacity: + * @return: Value between 0 (transparent) and 255 (fully opaque). + */ + getCrosshairsOpacity: function() { + if (this._crossHairs) + return this._crossHairs.getOpacity(); + else + return 0; + }, + + /** + * setCrosshairsLength: + * Set the crosshairs length for all ZoomRegions. + * @length: The length of the vertical and horizontal lines making up the + * crosshairs. + */ + setCrosshairsLength: function(length) { + if (this._crossHairs) + this._crossHairs.setLength(length); + }, + + /** + * getCrosshairsLength: + * Get the crosshairs length. + * @return: The length of the vertical and horizontal lines making up the + * crosshairs. + */ + getCrosshairsLength: function() { + if (this._crossHairs) + return this._crossHairs.getLength(); + else + return 0; + }, + + /** + * setCrosshairsClip: + * Set whether the crosshairs are clipped at their intersection. + * @clip: Flag to indicate whether to clip the crosshairs. + */ + setCrosshairsClip: function(clip) { + if (clip) { + if (this._crossHairs) + this._crossHairs.setClip(DEFAULT_CROSSHAIRS_CLIP_SIZE); + } + else { + // Setting no clipping on crosshairs means a zero sized clip + // rectangle. + if (this._crossHairs) + this._crossHairs.setClip([0, 0]); + } + }, + + /** + * getCrosshairsClip: + * Get whether the crosshairs are clipped by the mouse image. + * @return: Whether the crosshairs are clipped. + */ + getCrosshairsClip: function() { + if (this._crossHairs) { + let [clipWidth, clipHeight] = this._crossHairs.getClip(); + return (clipWidth > 0 && clipHeight > 0); + } + else + return false; + }, + + //// Private methods //// + + _updateMouseSprite: function() { + this._xfixesCursor.update_texture_image(this._mouseSprite); + let xHot = this._xfixesCursor.get_hot_x(); + let yHot = this._xfixesCursor.get_hot_y(); + this._mouseSprite.set_anchor_point(xHot, yHot); + }, + + _gConfInit: function(zoomRegion) { + let gConf = Shell.GConf.get_default(); + if (zoomRegion) { + // Mag factor is accurate to two decimal places. + let aPref = parseFloat(gConf.get_float(MAG_FACTOR_KEY).toFixed(2)); + if (aPref != 0.0) + zoomRegion.setMagFactor(aPref, aPref); + + aPref = gConf.get_int(SCREEN_POSITION_KEY); + if (aPref) + zoomRegion.setScreenPosition(aPref); + + zoomRegion.setLensMode(gConf.get_boolean(LENS_MODE_KEY)); + zoomRegion.setClampScrollingAtEdges(!gConf.get_boolean(CLAMP_MODE_KEY)); + + aPref = gConf.get_int(MOUSE_TRACKING_KEY); + if (aPref) + zoomRegion.setMouseTrackingMode(aPref); + } + let showCrosshairs = gConf.get_boolean(SHOW_CROSS_HAIRS_KEY); + let thickness = gConf.get_int(CROSS_HAIRS_THICKNESS_KEY); + let color = gConf.get_string(CROSS_HAIRS_COLOR_KEY); + let opacity = gConf.get_int(CROSS_HAIRS_OPACITY_KEY); + let length = gConf.get_int(CROSS_HAIRS_LENGTH_KEY); + let clip = gConf.get_boolean(CROSS_HAIRS_CLIP_KEY); + this.addCrosshairs(thickness, color, opacity, length, clip); + this.setCrosshairsVisible(showCrosshairs); + + gConf.watch_directory(A11Y_MAG_PREFS_DIR); + gConf.connect('changed::' + SHOW_KEY, Lang.bind(this, this._updateShowHide)); + gConf.connect('changed::' + SCREEN_POSITION_KEY, Lang.bind(this, this._updateScreenPosition)); + gConf.connect('changed::' + MAG_FACTOR_KEY, Lang.bind(this, this._updateMagFactor)); + gConf.connect('changed::' + LENS_MODE_KEY, Lang.bind(this, this._updateLensMode)); + gConf.connect('changed::' + CLAMP_MODE_KEY, Lang.bind(this, this._updateClampMode)); + gConf.connect('changed::' + MOUSE_TRACKING_KEY, Lang.bind(this, this._updateMouseTrackingMode)); + gConf.connect('changed::' + SHOW_CROSS_HAIRS_KEY, Lang.bind(this, this._updateShowCrosshairs)); + gConf.connect('changed::' + CROSS_HAIRS_THICKNESS_KEY, Lang.bind(this, this._updateCrosshairsThickness)); + gConf.connect('changed::' + CROSS_HAIRS_COLOR_KEY, Lang.bind(this, this._updateCrosshairsColor)); + gConf.connect('changed::' + CROSS_HAIRS_OPACITY_KEY, Lang.bind(this, this._updateCrosshairsOpacity)); + gConf.connect('changed::' + CROSS_HAIRS_LENGTH_KEY, Lang.bind(this, this._updateCrosshairsLength)); + gConf.connect('changed::' + CROSS_HAIRS_CLIP_KEY, Lang.bind(this, this._updateCrosshairsClip)); + + return gConf.get_boolean(SHOW_KEY); + }, + + _updateShowHide: function() { + let gConf = Shell.GConf.get_default(); + this.setActive(gConf.get_boolean(SHOW_KEY)); + }, + + _updateScreenPosition: function() { + // Applies only to the first zoom region. + if (this._zoomRegions.length) { + let gConf = Shell.GConf.get_default(); + let position = gConf.get_int(SCREEN_POSITION_KEY); + this._zoomRegions[0].setScreenPosition(position); + if (position != ScreenPosition.FULL_SCREEN) + this._updateLensMode(); + } + }, + + _updateMagFactor: function() { + // Applies only to the first zoom region. + if (this._zoomRegions.length) { + let gConf = Shell.GConf.get_default(); + // Mag factor is accurate to two decimal places. + let magFactor = parseFloat(gConf.get_float(MAG_FACTOR_KEY).toFixed(2)); + this._zoomRegions[0].setMagFactor(magFactor, magFactor); + } + }, + + _updateLensMode: function() { + // Applies only to the first zoom region. + if (this._zoomRegions.length) { + let gConf = Shell.GConf.get_default(); + this._zoomRegions[0].setLensMode(gConf.get_boolean(LENS_MODE_KEY)); + } + }, + + _updateClampMode: function() { + // Applies only to the first zoom region. + if (this._zoomRegions.length) { + let gConf = Shell.GConf.get_default(); + this._zoomRegions[0].setClampScrollingAtEdges( + !gConf.get_boolean(CLAMP_MODE_KEY) + ); + } + }, + + _updateMouseTrackingMode: function() { + // Applies only to the first zoom region. + if (this._zoomRegions.length) { + let gConf = Shell.GConf.get_default(); + this._zoomRegions[0].setMouseTrackingMode( + gConf.get_int(MOUSE_TRACKING_KEY) + ); + } + }, + + _updateShowCrosshairs: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsVisible(gConf.get_boolean(SHOW_CROSS_HAIRS_KEY)); + }, + + _updateCrosshairsThickness: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsThickness(gConf.get_int(CROSS_HAIRS_THICKNESS_KEY)); + }, + + _updateCrosshairsColor: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsColor(gConf.get_string(CROSS_HAIRS_COLOR_KEY)); + }, + + _updateCrosshairsOpacity: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsOpacity(gConf.get_int(CROSS_HAIRS_OPACITY_KEY)); + }, + + _updateCrosshairsLength: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsLength(gConf.get_int(CROSS_HAIRS_LENGTH_KEY)); + }, + + _updateCrosshairsClip: function() { + let gConf = Shell.GConf.get_default(); + this.setCrosshairsClip(gConf.get_boolean(CROSS_HAIRS_CLIP_KEY)); + } +} + +function ZoomRegion(magnifier, mouseRoot) { + this._init(magnifier, mouseRoot); +} + +ZoomRegion.prototype = { + _init: function(magnifier, mouseRoot) { + this._magnifier = magnifier; + + // The root actor for the zoom region + this._magView = new St.Bin({ style_class: 'magnifier-zoom-region', x_fill: true, y_fill: true }); + global.stage.add_actor(this._magView); + this._magView.hide(); + + // Append a Clutter.Group to clip the contents of the magnified view. + this._mainGroup = new Clutter.Group({ clip_to_allocation: true }); + this._magView.set_child(this._mainGroup); + + // Add a background for when the magnified uiGroup is scrolled + // out of view (don't want to see desktop showing through). + let background = new Clutter.Rectangle({ color: Main.DEFAULT_BACKGROUND_COLOR }); + this._mainGroup.add_actor(background); + + // Clone the group that contains all of UI on the screen. This is the + // chrome, the windows, etc. + this._uiGroupClone = new Clutter.Clone({ source: Main.uiGroup }); + this._mainGroup.add_actor(this._uiGroupClone); + Main.uiGroup.set_size(global.screen_width, global.screen_height); + background.set_size(global.screen_width, global.screen_height); + this._uiGroupClone.set_size(global.screen_width, global.screen_height); + + // Add either the given mouseRoot to the ZoomRegion, or a clone of + // it. + if (mouseRoot.get_parent() != null) + this._mouseRoot = new Clutter.Clone({ source: mouseRoot }); + else + this._mouseRoot = mouseRoot; + this._mainGroup.add_actor(this._mouseRoot); + this._crossHairs = null; + + this.setMagFactor(DEFAULT_X_MAGFACTOR, DEFAULT_Y_MAGFACTOR); + this.setScreenPosition(DEFAULT_SCREEN_POSITION); + this.setLensMode(DEFAULT_LENS_MODE); + this.setClampScrollingAtEdges(DEFAULT_CLAMP_SCROLLING_AT_EDGES); + this.setMouseTrackingMode(DEFAULT_MOUSE_TRACKING_MODE); + }, + + /** + * setActive: + * @activate: Boolean to show/hide the ZoomRegion. + */ + setActive: function(activate) { + if (activate) { + this._magView.show(); + if (this.isMouseOverRegion()) + this._magnifier.hideSystemCursor(); + this._updateMousePosition(false /* mouse didn't move */); + } + else + this._magView.hide(); + }, + + /** + * isActive: + * @return Whether this ZoomRegion is active (boolean). + */ + isActive: function() { + return this._magView.visible; + }, + + /** + * removeFromStage: + * Remove the magnified view from the stage. + */ + removeFromStage: function() { + global.stage.remove_actor(this._magView); + this._mouseRoot = null; + this._uiGroupClone = null; + this._magView = null; + }, + + /** + * setMagFactor: + * @xMagFactor: The power to set the horizontal magnification factor to + * of the magnified view. A value of 1.0 means no + * magnification. A value of 2.0 doubles the size. + * @yMagFactor: The power to set the vertical magnification factor to + * of the magnified view. + */ + setMagFactor: function(xMagFactor, yMagFactor) { + if (xMagFactor > 0 && yMagFactor > 0) { + // Changing the mag factor moves the pixels along the axes of + // magnification. Set the view back to the point that was at the centre + // of the region of interest. + let [x, y, width, height] = this.getROI(); + let xCentre = x + width / 2; + let yCentre = y + height / 2; + this._uiGroupClone.set_scale(xMagFactor, yMagFactor); + this._mouseRoot.set_scale(xMagFactor, yMagFactor); + this._calcRightBottomStops(); + this._scrollToPosition(xCentre, yCentre); + this._updateMousePosition(false /* mouse didn't move */); + } + }, + + /** + * getMagFactor: + * @return an array, [xMagFactor, yMagFactor], containing the horizontal + * and vertical magnification powers. A value of 1.0 means no + * magnification. A value of 2.0 means the contents are doubled + * in size, and so on. + */ + getMagFactor: function() { + return this._uiGroupClone.get_scale(); + }, + + /** + * setMouseTrackingMode + * @mode: One of the enum MouseTrackingMode values. + */ + setMouseTrackingMode: function(mode) { + if (mode >= MouseTrackingMode.NONE && mode <= MouseTrackingMode.PROPORTIONAL) + this._mouseTrackingMode = mode; + }, + + /** + * getMouseTrackingMode + * @return: One of the enum MouseTrackingMode values. + */ + getMouseTrackingMode: function() { + return this._mouseTrackingMode; + }, + + /** + * setViewPort + * Sets the position and size of the ZoomRegion on screen. + * @viewPort: Object defining the position and size of the view port. It + * has the form { x, y, width, height }. The values are in + * stage coordinate space. + */ + setViewPort: function(viewPort) { + let [xRoi, yRoi, wRoi, hRoi] = this.getROI(); + + // Remove border if the view port is the entire screen. Otherwise, + // ensure that the border is there. + if (viewPort.x == 0 && viewPort.y == 0 && viewPort.width == global.screen_width && viewPort.height == global.screen_height) + this._magView.add_style_class_name('full-screen'); + else + this._magView.remove_style_class_name('full-screen'); + + this.setSize(viewPort.width, viewPort.height); + this.setPosition(viewPort.x, viewPort.y); + if (this._crossHairs) + this._crossHairs.reCenter(); + + this.scrollContentsTo(xRoi + wRoi / 2, yRoi + hRoi / 2); + if (this.isMouseOverRegion()) + this._magnifier.hideSystemCursor(); + + this._screenPosition = ScreenPosition.NONE; + }, + + /** + * setROI + * Sets the "region of interest" that the ZoomRegion is magnifying. + * @roi: Object that defines the region of the screen to magnify. It + * has the form { x, y, width, height }. The values are in + * screen (unmagnified) coordinate space. + */ + setROI: function(roi) { + let xRoiCenter = roi.x + roi.width / 2; + let yRoiCenter = roi.y + roi.height / 2; + this.scrollContentsTo(xRoiCenter, yRoiCenter); + }, + + /** + * setSize: + * @width: The width to set the magnified view to. + * @height: The height to set the magnified view to. + */ + setSize: function(width, height) { + this._magView.set_size(width, height); + this._calcRightBottomStops(); + }, + + /** + * getSize: + * @return an array, [width, height], that specifies the size of the + * magnified view. + */ + getSize: function() { + return this._magView.get_size(); + }, + + /** + * setPosition: + * Position the magnified view at the given coordinates. + * @x: The x-coord of the new position. + * @y: The y-coord of the new position. + */ + setPosition: function(x, y) { + let [width, height] = this._magView.get_size(); + if (this._clampScrollingAtEdges) { + // Restrict positioning so view doesn't go beyond any edge of the + // screen. + if (x < 0) + x = 0; + if (x + width > global.screen_width) + x = global.screen_width - width; + if (y < 0) + y = 0; + if (y + height > global.screen_height) + y = global.screen_height - height; + } + this._magView.set_position(x, y); + }, + + /** + * getPosition: + * @return an array, [x, y], that gives the position of the + * magnified view on screen. + */ + getPosition: function() { + return this._magView.get_position(); + }, + + /** + * getCenter: + * @return an array, [x, y], that is half the width and height of the + * magnified view (the center of the magnified view). + */ + getCenter: function() { + let [width, height] = this._magView.get_size(); + return [width / 2, height / 2]; + }, + + /** + * isFullScreenMode: + * Does the magnified view occupy the whole screen? + */ + isFullScreenMode: function() { + let [x, y] = this._magView.get_position(); + if (x != 0 || y != 0) + return false; + [width, height] = this._magView.get_size(); + if (width != global.screen_width || height != global.screen_height) + return false; + return true; + }, + + /** + * getROI: + * Retrieves the "region of interest" -- the rectangular bounds of that part + * of the desktop that the magnified view is showing (x, y, width, height). + * The bounds are given in non-magnified coordinates. + * @return an array, [x, y, width, height], representing the bounding + * rectangle of what is shown in the magnified view. + */ + getROI: function() { + let [xMagnified, yMagnified] = this._uiGroupClone.get_position(); + let [xMagFactor, yMagFactor] = this.getMagFactor(); + let [width, height] = this.getSize(); + let x = (0 - xMagnified) / xMagFactor; + let y = (0 - yMagnified) / yMagFactor; + return [x, y, width / xMagFactor, height / yMagFactor]; + }, + + /** + * setLensMode: + * Turn lens mode on/off. In full screen mode, lens mode is alway off since + * a lens the size of the screen is pointless. + * @lensMode: A boolean to set the sense of lens mode. + */ + setLensMode: function(lensMode) { + let fullScreen = this.isFullScreenMode(); + this._lensMode = (lensMode && !fullScreen); + if (!this._lensMode && !fullScreen) + this.setScreenPosition (this._screenPosition); + }, + + /** + * isLensMode: + * Is lens mode on or off? + * @return The lens mode state as a boolean. + */ + isLensMode: function() { + return this._lensMode; + }, + + /** + * setClampScrollingAtEdges: + * Stop vs. allow scrolling of the magnified contents when it scroll beyond + * the edges of the screen. + * @clamp: Boolean to turn on/off clamping. + */ + setClampScrollingAtEdges: function(clamp) { + this._clampScrollingAtEdges = clamp; + }, + + /** + * setTopHalf: + * Magnifier view occupies the top half of the screen. + */ + setTopHalf: function() { + let viewPort = {}; + viewPort.x = 0; + viewPort.y = 0; + viewPort.width = global.screen_width; + viewPort.height = global.screen_height/2; + this.setViewPort(viewPort); + this._screenPosition = ScreenPosition.TOP_HALF; + }, + + /** + * setBottomHalf: + * Magnifier view occupies the bottom half of the screen. + */ + setBottomHalf: function() { + let viewPort = {}; + viewPort.x = 0; + viewPort.y = global.screen_height/2; + viewPort.width = global.screen_width; + viewPort.height = global.screen_height/2; + this.setViewPort(viewPort); + this._screenPosition = ScreenPosition.BOTTOM_HALF; + }, + + /** + * setLeftHalf: + * Magnifier view occupies the left half of the screen. + */ + setLeftHalf: function() { + let viewPort = {}; + viewPort.x = 0; + viewPort.y = 0; + viewPort.width = global.screen_width/2; + viewPort.height = global.screen_height; + this.setViewPort(viewPort); + this._screenPosition = ScreenPosition.LEFT_HALF; + }, + + /** + * setRightHalf: + * Magnifier view occupies the right half of the screen. + */ + setRightHalf: function() { + let viewPort = {}; + viewPort.x = global.screen_width/2; + viewPort.y = 0; + viewPort.width = global.screen_width/2; + viewPort.height = global.screen_height; + this.setViewPort(viewPort); + this._screenPosition = ScreenPosition.RIGHT_HALF; + }, + + /** + * getScreenPosition: + * Tell the outside world what the current mode is -- magnifiying the + * top half, bottom half, etc. + * @return: the current mode. + */ + getScreenPosition: function() { + return this._screenPosition; + }, + + /** + * scrollToMousePos: + * Set the region of interest based on the position of the system pointer. + * @return: Whether the system mouse pointer is over the magnified view. + */ + scrollToMousePos: function() { + let [objUnder, xMouse, yMouse, mask] = + Gdk.Screen.get_default().get_root_window().get_pointer(); + + if (this._mouseTrackingMode == MouseTrackingMode.PROPORTIONAL) { + this._setROIProportional(xMouse, yMouse); + } + else if (this._mouseTrackingMode == MouseTrackingMode.PUSH) { + this._setROIPush(xMouse, yMouse); + } + else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) { + this._setROICentered(xMouse, yMouse); + } + this._updateMousePosition(true); + + // Determine whether the system mouse pointer is over this zoom region. + return this.isMouseOverRegion(xMouse, yMouse); + }, + + /** + * setFullScreenMode: + * Set the ZoomRegion to full-screen mode. + * Note: disallows lens mode. + */ + setFullScreenMode: function() { + if (!this.isFullScreenMode()) { + let viewPort = {}; + viewPort.x = 0; + viewPort.y = 0; + viewPort.width = global.screen_width; + viewPort.height = global.screen_height; + this.setViewPort(viewPort); + this.setLensMode(false); + if (this.isActive()) + this._magnifier.hideSystemCursor(); + + this._screenPosition = ScreenPosition.FULL_SCREEN; + } + }, + + /** + * setScreenPosition: + * Positions the zoom region to one of the enumerated positions on the + * screen. + * @position: one of Magnifier.FULL_SCREEN, Magnifier.TOP_HALF, + * Magnifier.BOTTOM_HALF,Magnifier.LEFT_HALF, or + * Magnifier.RIGHT_HALF. + */ + setScreenPosition: function(inPosition) { + switch (inPosition) { + case ScreenPosition.FULL_SCREEN: + this.setFullScreenMode(); + break; + case ScreenPosition.TOP_HALF: + this.setTopHalf(); + break; + case ScreenPosition.BOTTOM_HALF: + this.setBottomHalf(); + break; + case ScreenPosition.LEFT_HALF: + this.setLeftHalf(); + break; + case ScreenPosition.RIGHT_HALF: + this.setRightHalf(); + break; + } + }, + + /** + * scrollContentsTo: + * Shift the contents of the magnified view such it is centered on the given + * coordinate. Also, update the position of the magnified mouse image after + * the shift. + * @x: The x-coord of the point to center on. + * @y: The y-coord of the point to center on. + */ + scrollContentsTo: function(x, y) { + this._scrollToPosition(x, y); + this._updateMousePosition(false /* mouse didn't move */); + }, + + /** + * isMouseOverRegion: + * Return whether the system mouse sprite is over this ZoomRegion. If the + * mouse's position is not given, then it is fetched. + * @xMouse: The system mouse's x-coord. Optional. + * @yMouse: The system mouse's y-coord. Optional. + * @return: Boolean: true if the mouse is over the zoom region; false + * otherwise. + */ + isMouseOverRegion: function(xMouse, yMouse) { + let mouseIsOver = false; + if (this.isActive()) { + if (!xMouse || !yMouse) { + let [objUnder, x, y, mask] = + Gdk.Screen.get_default().get_root_window().get_pointer(); + xMouse = x; + yMouse = y; + } + let [x, y] = this.getPosition(); + let [width, height] = this.getSize(); + mouseIsOver = ( + xMouse >= x && xMouse < (x + width) && + yMouse >= y && yMouse < (y + height) + ); + } + return mouseIsOver; + }, + + /** + * addCrosshairs: + * Add crosshairs centered on the magnified mouse. + * @crossHairs Clutter.Group that contains the actors for the crosshairs. + */ + addCrosshairs: function(crossHairs) { + // If the crossHairs is not already within a larger container, add it + // to this zoom region. Otherwise, add a clone. + if (crossHairs) { + this._crosshairsActor = crossHairs.addToZoomRegion(this, this._mouseRoot); + this._crossHairs = crossHairs; + } + }, + + //// Private methods //// + + _scrollToPosition: function(x, y) { + // Given the point (x, y) in non-magnified coordinates, scroll the + // magnified contenst such that the point is at the centre of the + // magnified view. + let [xMagFactor, yMagFactor] = this.getMagFactor(); + let xMagnified = x * xMagFactor; + let yMagnified = y * yMagFactor; + + let [xCenterMagView, yCenterMagView] = this.getCenter(); + let newX = xCenterMagView - xMagnified; + let newY = yCenterMagView - yMagnified; + + if (this._clampScrollingAtEdges) { + if (newX > 0) + newX = 0; + else if (newX < this._rightStop) + newX = this._rightStop; + if (newY > 0) + newY = 0; + else if (newY < this._bottomStop) + newY = this._bottomStop; + this._uiGroupClone.set_position(newX, newY); + } + else + this._uiGroupClone.set_position(newX, newY); + + // If in lens mode, move the magnified view such that it is centered + // over the actual mouse. However, in full screen mode, the "lens" is + // the size of the screen -- pointless to move such a large lens around. + if (this._lensMode && !this.isFullScreenMode()) + this.setPosition(x - xCenterMagView, y - yCenterMagView); + }, + + _calcRightBottomStops: function() { + // Calculate the location of the top-left corner of _uiGroupClone + // when its right and bottom edges are coincident with the right and + // bottom edges of the _magView. + let [contentWidth, contentHeight] = this._uiGroupClone.get_size(); + let [viewWidth, viewHeight] = this.getSize(); + let [xMagFactor, yMagFactor] = this.getMagFactor(); + let rightStop = viewWidth - (contentWidth * xMagFactor); + let bottomStop = viewHeight - (contentHeight * yMagFactor); + this._rightStop = parseInt(rightStop.toFixed(1)); + this._bottomStop = parseInt(bottomStop.toFixed(1)); + }, + + _setROIPush: function(xMouse, yMouse) { + let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI(); + let [cursorWidth, cursorHeight] = this._mouseRoot.get_size(); + let xPos = xRoi + widthRoi / 2; + let yPos = yRoi + heightRoi / 2; + let xRoiRight = xRoi + widthRoi - cursorWidth; + let yRoiBottom = yRoi + heightRoi - cursorHeight; + + if (xMouse < xRoi) + xPos -= (xRoi - xMouse); + else if (xMouse > xRoiRight) + xPos += (xMouse - xRoiRight); + + if (yMouse < yRoi) + yPos -= (yRoi - yMouse); + else if (yMouse > yRoiBottom) + yPos += (yMouse - yRoiBottom); + + this._scrollToPosition(xPos, yPos); + }, + + _setROIProportional: function(xMouse, yMouse) { + let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI(); + let halfScreenWidth = global.screen_width / 2; + let halfScreenHeight = global.screen_height / 2; + let xProportion = (halfScreenWidth - xMouse) / halfScreenWidth; + let yProportion = (halfScreenHeight - yMouse) / halfScreenHeight; + let xPos = xMouse + xProportion * widthRoi / 2; + let yPos = yMouse + yProportion * heightRoi / 2; + + this._scrollToPosition(xPos, yPos); + }, + + _setROICentered: function(xMouse, yMouse) { + this._scrollToPosition(xMouse, yMouse); + }, + + _updateMousePosition: function(mouseMoved) { + let [x, y] = this._uiGroupClone.get_position(); + x = parseInt(x.toFixed(1)); + y = parseInt(y.toFixed(1)); + let [xCenterMagView, yCenterMagView] = this.getCenter(); + let [objUnder, xMouse, yMouse, mask] = + Gdk.Screen.get_default().get_root_window().get_pointer(); + let [xMagFactor, yMagFactor] = this.getMagFactor(); + + let xMagMouse = xMouse * xMagFactor + x; + let yMagMouse = yMouse * yMagFactor + y; + if (mouseMoved) { + if (x == 0) + xMagMouse = xMouse * xMagFactor; + else if (x == this._rightStop) + xMagMouse = (xMouse * xMagFactor) + this._rightStop; + else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) + xMagMouse = xCenterMagView; + + if (y == 0) + yMagMouse = yMouse * yMagFactor; + else if (y == this._bottomStop) + yMagMouse = (yMouse * yMagFactor) + this._bottomStop; + else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) + yMagMouse = yCenterMagView; + } + this._mouseRoot.set_position(xMagMouse, yMagMouse); + this._updateCrosshairsPosition(xMagMouse, yMagMouse); + }, + + _updateCrosshairsPosition: function(x, y) { + if (this._crosshairsActor) { + let [groupWidth, groupHeight] = this._crosshairsActor.get_size(); + this._crosshairsActor.set_position(x - groupWidth / 2, y - groupHeight / 2); + } + } +} + +function Crosshairs() { + this._init(); +} + +Crosshairs.prototype = { + _init: function() { + + // Set the group containing the crosshairs to three times the desktop + // size in case the crosshairs need to appear to be infinite in + // length (i.e., extend beyond the edges of the view they appear in). + let groupWidth = global.screen_width * 3; + let groupHeight = global.screen_height * 3; + this._actor = new Clutter.Group({ + clip_to_allocation: false, + width: groupWidth, + height: groupHeight + }); + this._horizLeftHair = new Clutter.Rectangle({ + color: DEFAULT_CROSSHAIRS_COLOR, + width: groupWidth / 2, + height: DEFAULT_CROSSHAIRS_THICKNESS, + opacity: DEFAULT_CROSSHAIRS_OPACITY + }); + this._horizRightHair = new Clutter.Rectangle({ + color: DEFAULT_CROSSHAIRS_COLOR, + width: groupWidth / 2, + height: DEFAULT_CROSSHAIRS_THICKNESS, + opacity: DEFAULT_CROSSHAIRS_OPACITY + }); + this._vertTopHair = new Clutter.Rectangle({ + color: DEFAULT_CROSSHAIRS_COLOR, + width: DEFAULT_CROSSHAIRS_THICKNESS, + height: groupHeight / 2, + opacity: DEFAULT_CROSSHAIRS_OPACITY + }); + this._vertBottomHair = new Clutter.Rectangle({ + color: DEFAULT_CROSSHAIRS_COLOR, + width: DEFAULT_CROSSHAIRS_THICKNESS, + height: groupHeight / 2, + opacity: DEFAULT_CROSSHAIRS_OPACITY + }); + this._actor.add_actor(this._horizLeftHair); + this._actor.add_actor(this._horizRightHair); + this._actor.add_actor(this._vertTopHair); + this._actor.add_actor(this._vertBottomHair); + this._clipSize = [0, 0]; + this._clones = []; + this.reCenter(); + }, + + /** + * addToZoomRegion + * Either add the crosshairs actor to the given ZoomRegion, or, if it is + * already part of some other ZoomRegion, create a clone of the crosshairs + * actor, and add the clone instead. Returns either the original or the + * clone. + * @zoomRegion: The container to add the crosshairs group to. + * @magnifiedMouse: The mouse actor for the zoom region -- used to + * position the crosshairs and properly layer them below + * the mouse. + * @return The crosshairs actor, or its clone. + */ + addToZoomRegion: function(zoomRegion, magnifiedMouse) { + let crosshairsActor = null; + if (zoomRegion && magnifiedMouse) { + let container = magnifiedMouse.get_parent(); + if (container) { + crosshairsActor = this._actor; + if (this._actor.get_parent() != null) { + crosshairsActor = new Clutter.Clone({ source: this._actor }); + this._clones.push(crosshairsActor); + } + if (this._actor.visible) + crosshairsActor.show(); + else + crosshairsActor.hide(); + + container.add_actor(crosshairsActor); + container.raise_child(magnifiedMouse, crosshairsActor); + let [xMouse, yMouse] = magnifiedMouse.get_position(); + let [crosshairsWidth, crosshairsHeight] = crosshairsActor.get_size(); + crosshairsActor.set_position(xMouse - crosshairsWidth / 2 , yMouse - crosshairsHeight / 2); + } + } + return crosshairsActor; + }, + + /** + * removeFromParent: + * Remove the crosshairs actor from its parent container. + */ + removeFromParent: function() { + this._actor.get_parent().remove_actor(this._actor); + }, + + /** + * setColor: + * Set the color of the crosshairs. + * @clutterColor: The color as a Clutter.Color. + */ + setColor: function(clutterColor) { + this._horizLeftHair.set_color(clutterColor); + this._horizRightHair.set_color(clutterColor); + this._vertTopHair.set_color(clutterColor); + this._vertBottomHair.set_color(clutterColor); + }, + + /** + * getColor: + * Get the color of the crosshairs. + * @color: The color as a Clutter.Color. + */ + getColor: function() { + let clutterColor = new Clutter.Color(); + this._horizLeftHair.get_color(clutterColor); + return clutterColor; + }, + + /** + * setThickness: + * Set the width of the vertical and horizontal lines of the crosshairs. + * @thickness + */ + setThickness: function(thickness) { + this._horizLeftHair.set_height(thickness); + this._horizRightHair.set_height(thickness); + this._vertTopHair.set_width(thickness); + this._vertBottomHair.set_width(thickness); + this.reCenter(); + }, + + /** + * getThickness: + * Get the width of the vertical and horizontal lines of the crosshairs. + * @return: The thickness of the crosshairs. + */ + getThickness: function() { + return this._horizLeftHair.get_height(); + }, + + /** + * setOpacity: + * Set how opaque the crosshairs are. + * @opacity: Value between 0 (fully transparent) and 255 (full opaque). + */ + setOpacity: function(opacity) { + // set_opacity() throws an exception for values outside the range + // [0, 255]. + if (opacity < 0) + opacity = 0; + else if (opacity > 255) + opacity = 255; + + this._horizLeftHair.set_opacity(opacity); + this._horizRightHair.set_opacity(opacity); + this._vertTopHair.set_opacity(opacity); + this._vertBottomHair.set_opacity(opacity); + }, + + /** + * getOpacity: + * Retriev how opaque the crosshairs are. + * @return: A value between 0 (transparent) and 255 (opaque). + */ + getOpacity: function() { + return this._horizLeftHair.get_opacity(); + }, + + /** + * setLength: + * Set the length of the vertical and horizontal lines in the crosshairs. + * @length: The length of the crosshairs. + */ + setLength: function(length) { + this._horizLeftHair.set_width(length); + this._horizRightHair.set_width(length); + this._vertTopHair.set_height(length); + this._vertBottomHair.set_height(length); + this.reCenter(); + }, + + /** + * getLength: + * Get the length of the vertical and horizontal lines in the crosshairs. + * @return: The length of the crosshairs. + */ + getLength: function() { + return this._horizLeftHair.get_width(); + }, + + /** + * setClip: + * Set the width and height of the rectangle that clips the crosshairs at + * their intersection + * @size: Array of [width, height] defining the size of the clip + * rectangle. + */ + setClip: function(size) { + if (size) { + // Take a chunk out of the crosshairs where it intersects the + // mouse. + this._clipSize = size; + this.reCenter(); + } + else { + // Restore the missing chunk. + this._clipSize = [0, 0]; + this.reCenter(); + } + }, + + /** + * getClip: + * Get the dimensions of the clip rectangle. + * @return: An array of the form [width, height]. + */ + getClip: function() { + return this._clipSize; + }, + + /** + * show: + * Show the crosshairs. + */ + show: function() { + this._actor.show(); + // Clones don't share visibility. + for (let i = 0; i < this._clones.length; i++) + this._clones[i].show(); + }, + + /** + * hide: + * Hide the crosshairs. + */ + hide: function() { + this._actor.hide(); + // Clones don't share visibility. + for (let i = 0; i < this._clones.length; i++) + this._clones[i].hide(); + }, + + /** + * reCenter: + * Reposition the horizontal and vertical hairs such that they cross at + * the center of crosshairs group. If called with the dimensions of + * the clip rectangle, these are used to update the size of the clip. + * @clipSize: Optional. If present, an array of the form [width, height]. + */ + reCenter: function(clipSize) { + let [groupWidth, groupHeight] = this._actor.get_size(); + let leftLength = this._horizLeftHair.get_width(); + let rightLength = this._horizRightHair.get_width(); + let topLength = this._vertTopHair.get_height(); + let bottomLength = this._vertBottomHair.get_height(); + let thickness = this._horizLeftHair.get_height(); + + // Deal with clip rectangle. + if (clipSize) + this._clipSize = clipSize; + let clipWidth = this._clipSize[0]; + let clipHeight = this._clipSize[1]; + + // Note that clip, if present, is not centred on the cross hair + // intersection, but biased towards the top left. + let left = groupWidth / 2 - clipWidth * 0.25 - leftLength; + let right = groupWidth / 2 + clipWidth * 0.75; + let top = groupHeight / 2 - clipHeight * 0.25 - topLength - thickness / 2; + let bottom = groupHeight / 2 + clipHeight * 0.75 + thickness / 2; + this._horizLeftHair.set_position(left, (groupHeight - thickness) / 2); + this._horizRightHair.set_position(right, (groupHeight - thickness) / 2); + this._vertTopHair.set_position((groupWidth - thickness) / 2, top); + this._vertBottomHair.set_position((groupWidth - thickness) / 2, bottom); + } + } diff --git a/js/ui/magnifierDBus.js b/js/ui/magnifierDBus.js new file mode 100644 index 000000000..08a3cee0b --- /dev/null +++ b/js/ui/magnifierDBus.js @@ -0,0 +1,375 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const DBus = imports.dbus; +const Main = imports.ui.main; + +const MAG_SERVICE_NAME = 'org.gnome.Magnifier'; +const MAG_SERVICE_PATH = '/org/gnome/Magnifier'; +const ZOOM_SERVICE_NAME = 'org.gnome.Magnifier.ZoomRegion'; +const ZOOM_SERVICE_PATH = '/org/gnome/Magnifier/ZoomRegion'; + +// Subset of gnome-mag's Magnifier dbus interface -- to be expanded. See: +// http://git.gnome.org/browse/gnome-mag/tree/xml/...Magnifier.xml +const MagnifierIface = { + name: MAG_SERVICE_NAME, + methods: [ + { name: 'setActive', inSignature: 'b', outSignature: '' }, + { name: 'isActive', inSignature: '', outSignature: 'b' }, + { name: 'showCursor', inSignature: '', outSignature: '' }, + { name: 'hideCursor', inSignature: '', outSignature: '' }, + { name: 'createZoomRegion', inSignature: 'ddaiai', outSignature: 'o' }, + { name: 'addZoomRegion', inSignature: 'o', outSignature: 'b' }, + { name: 'getZoomRegions', inSignature: '', outSignature: 'ao' }, + { name: 'clearAllZoomRegions', inSignature: '', outSignature: '' }, + { name: 'fullScreenCapable', inSignature: '', outSignature: 'b' }, + + { name: 'setCrosswireSize', inSignature: 'i', outSignature: '' }, + { name: 'getCrosswireSize', inSignature: '', outSignature: 'i' }, + { name: 'setCrosswireLength', inSignature: 'i', outSignature: '' }, + { name: 'getCrosswireLength', inSignature: '', outSignature: 'i' }, + { name: 'setCrosswireClip', inSignature: 'b', outSignature: '' }, + { name: 'getCrosswireClip', inSignature: '', outSignature: 'b' }, + { name: 'setCrosswireColor', inSignature: 'u', outSignature: '' }, + { name: 'getCrosswireColor', inSignature: '', outSignature: 'u' } + ], + signals: [], + properties: [] +}; + +// Subset of gnome-mag's ZoomRegion dbus interface -- to be expanded. See: +// http://git.gnome.org/browse/gnome-mag/tree/xml/...ZoomRegion.xml +const ZoomRegionIface = { + name: ZOOM_SERVICE_NAME, + methods: [ + { name: 'setMagFactor', inSignature: 'dd', outSignature: ''}, + { name: 'getMagFactor', inSignature: '', outSignature: 'dd' }, + { name: 'setRoi', inSignature: 'ai', outSignature: '' }, + { name: 'getRoi', inSignature: '', outSignature: 'ai' }, + { name: 'shiftContentsTo', inSignature: 'ii', outSignature: 'b' }, + { name: 'moveResize', inSignature: 'ai', outSignature: '' } + ], + signals: [], + properties: [] +}; + +// For making unique ZoomRegion DBus proxy object paths of the form: +// "/org/gnome/Magnifier/ZoomRegion/zoomer0", +// "/org/gnome/Magnifier/ZoomRegion/zoomer1", etc. +let _zoomRegionInstanceCount = 0; + +function ShellMagnifier() { + this._init(); +} + +ShellMagnifier.prototype = { + _init: function() { + this._zoomers = {}; + DBus.session.exportObject(MAG_SERVICE_PATH, this); + }, + + /** + * setActive: + * @activate: Boolean to activate or de-activate the magnifier. + */ + setActive: function(activate) { + Main.magnifier.setActive(activate); + }, + + /** + * isActive: + * @return Whether the magnifier is active (boolean). + */ + isActive: function() { + return Main.magnifier.isActive(); + }, + + /** + * showCursor: + * Show the system mouse pointer. + */ + showCursor: function() { + Main.magnifier.showSystemCursor(); + }, + + /** + * hideCursor: + * Hide the system mouse pointer. + */ + hideCursor: function() { + Main.magnifier.hideSystemCursor(); + }, + + /** + * createZoomRegion: + * Create a new ZoomRegion and return its object path. + * @xMagFactor: The power to set horizontal magnification of the + * ZoomRegion. A value of 1.0 means no magnification. A + * value of 2.0 doubles the size. + * @yMagFactor: The power to set the vertical magnification of the + * ZoomRegion. + * @roi Array of integers defining the region of the + * screen/desktop to magnify. The array has the form + * [x, y, width, height]. + * @viewPort Array of integers, [ x, y, width, height ] that defines + * the position of the ZoomRegion on screen. + * @return The newly created ZoomRegion. + */ + createZoomRegion: function(xMagFactor, yMagFactor, roi, viewPort) { + let ROI = { x: roi[0], y: roi[1], width: roi[2], height: roi[3] }; + let viewBox = { x: viewPort[0], y: viewPort[1], width: viewPort[2], height: viewPort[3] }; + let realZoomRegion = Main.magnifier.createZoomRegion(xMagFactor, yMagFactor, ROI, viewBox); + let objectPath = ZOOM_SERVICE_PATH + "/zoomer" + _zoomRegionInstanceCount; + _zoomRegionInstanceCount++; + + let zoomRegionProxy = new ShellMagnifierZoomRegion(objectPath, realZoomRegion); + let proxyAndZoomRegion = {}; + proxyAndZoomRegion.proxy = zoomRegionProxy; + proxyAndZoomRegion.zoomRegion = realZoomRegion; + this._zoomers[objectPath] = proxyAndZoomRegion; + return objectPath; + }, + + /** + * addZoomRegion: + * Append the given ZoomRegion to the magnifier's list of ZoomRegions. + * @zoomerObjectPath: The object path for the zoom region proxy. + */ + addZoomRegion: function(zoomerObjectPath) { + let proxyAndZoomRegion = this._zoomers[zoomerObjectPath]; + if (proxyAndZoomRegion && proxyAndZoomRegion.zoomRegion) { + Main.magnifier.addZoomRegion(proxyAndZoomRegion.zoomRegion); + return true; + } + else + return false; + }, + + /** + * getZoomRegions: + * Return a list of ZoomRegion object paths for this Magnifier. + * @return: The Magnifier's zoom region list as an array of DBus object + * paths. + */ + getZoomRegions: function() { + // There may be more ZoomRegions in the magnifier itself than have + // been added through dbus. Make sure all of them are associated with + // an object path and proxy. + let zoomRegions = Main.magnifier.getZoomRegions(); + let objectPaths = []; + let thoseZoomers = this._zoomers; + zoomRegions.forEach (function(aZoomRegion, index, array) { + let found = false; + for (let objectPath in thoseZoomers) { + let proxyAndZoomRegion = thoseZoomers[objectPath]; + if (proxyAndZoomRegion.zoomRegion === aZoomRegion) { + objectPaths.push(objectPath); + found = true; + break; + } + } + if (!found) { + // Got a ZoomRegion with no DBus proxy, make one. + let newPath = ZOOM_SERVICE_PATH + "/zoomer" + _zoomRegionInstanceCount; + _zoomRegionInstanceCount++; + let zoomRegionProxy = new ShellMagnifierZoomRegion(newPath, aZoomRegion); + let proxyAndZoomer = {}; + proxyAndZoomer.proxy = zoomRegionProxy; + proxyAndZoomer.zoomRegion = aZoomRegion; + thoseZoomers[newPath] = proxyAndZoomer; + objectPaths.push(newPath); + } + }); + return objectPaths; + }, + + /** + * clearAllZoomRegions: + * Remove all the zoom regions from this Magnfier's ZoomRegion list. + */ + clearAllZoomRegions: function() { + Main.magnifier.clearAllZoomRegions(); + for (let objectPath in this._zoomers) { + let proxyAndZoomer = this._zoomers[objectPath]; + proxyAndZoomer.proxy = null; + proxyAndZoomer.zoomRegion = null; + delete this._zoomers[objectPath]; + DBus.session.unexportObject(proxyAndZoomer); + } + this._zoomers = {}; + }, + + /** + * fullScreenCapable: + * Consult if the Magnifier can magnify in full-screen mode. + * @return Always return true. + */ + fullScreenCapable: function() { + return true; + }, + + /** + * setCrosswireSize: + * Set the crosswire size of all ZoomRegions. + * @size: The thickness of each line in the cross wire. + */ + setCrosswireSize: function(size) { + Main.magnifier.setCrosshairsThickness(size); + }, + + /** + * getCrosswireSize: + * Get the crosswire size of all ZoomRegions. + * @return: The thickness of each line in the cross wire. + */ + getCrosswireSize: function() { + return Main.magnifier.getCrosshairsThickness(); + }, + + /** + * setCrosswireLength: + * Set the crosswire length of all zoom-regions.. + * @size: The length of each line in the cross wire. + */ + setCrosswireLength: function(length) { + Main.magnifier.setCrosshairsLength(length); + }, + + /** + * setCrosswireSize: + * Set the crosswire size of all zoom-regions. + * @size: The thickness of each line in the cross wire. + */ + getCrosswireLength: function() { + return Main.magnifier.getCrosshairsLength(); + }, + + /** + * setCrosswireClip: + * Set if the crosswire will be clipped by the cursor image.. + * @clip: Flag to indicate whether to clip the crosswire. + */ + setCrosswireClip: function(clip) { + Main.magnifier.setCrosshairsClip(clip); + }, + + /** + * getCrosswireClip: + * Get the crosswire clip value. + * @return: Whether the crosswire is clipped by the cursor image. + */ + getCrosswireClip: function() { + return Main.magnifier.getCrosshairsClip(); + }, + + /** + * setCrosswireColor: + * Set the crosswire color of all ZoomRegions. + * @color: Unsigned int of the form rrggbbaa. + */ + setCrosswireColor: function(color) { + Main.magnifier.setCrosshairsColor('#' + color.toString(16)); + }, + + /** + * getCrosswireClip: + * Get the crosswire color of all ZoomRegions. + * @return: The crosswire color as an unsigned int in the form rrggbbaa. + */ + getCrosswireColor: function() { + let colorString = Main.magnifier.getCrosshairsColor(); + // Drop the leading '#'. + return parseInt(colorString.slice(1), 16); + } +} + +/** + * ShellMagnifierZoomRegion: + * Object that implements the DBus ZoomRegion interface. + * @zoomerObjectPath: String that is the path to a DBus ZoomRegion. + * @zoomRegion: The actual zoom region associated with the object path. + */ +function ShellMagnifierZoomRegion(zoomerObjectPath, zoomRegion) { + this._init(zoomerObjectPath, zoomRegion); +} + +ShellMagnifierZoomRegion.prototype = { + _init: function(zoomerObjectPath, zoomRegion) { + this._zoomRegion = zoomRegion; + DBus.session.proxifyObject(this, ZOOM_SERVICE_NAME, zoomerObjectPath); + DBus.session.exportObject(zoomerObjectPath, this); + }, + + /** + * setMagFactor: + * @xMagFactor: The power to set the horizontal magnification factor to + * of the magnified view. A value of 1.0 means no + * magnification. A value of 2.0 doubles the size. + * @yMagFactor: The power to set the vertical magnification factor to + * of the magnified view. + */ + setMagFactor: function(xMagFactor, yMagFactor) { + this._zoomRegion.setMagFactor(xMagFactor, yMagFactor); + }, + + /** + * getMagFactor: + * @return an array, [xMagFactor, yMagFactor], containing the horizontal + * and vertical magnification powers. A value of 1.0 means no + * magnification. A value of 2.0 means the contents are doubled + * in size, and so on. + */ + getMagFactor: function() { + return this._zoomRegion.getMagFactor(); + }, + + /** + * setRoi: + * Sets the "region of interest" that the ZoomRegion is magnifying. + * @roi Array, [x, y, width, height], defining the region of the screen to + * magnify. The values are in screen (unmagnified) coordinate + * space. + */ + setRoi: function(roi) { + let roiObject = { x: roi[0], y: roi[1], width: roi[2], height: roi[3] }; + this._zoomRegion.setROI(roiObject); + }, + + /** + * getRoi: + * Retrieves the "region of interest" -- the rectangular bounds of that part + * of the desktop that the magnified view is showing (x, y, width, height). + * The bounds are given in non-magnified coordinates. + * @return an array, [x, y, width, height], representing the bounding + * rectangle of what is shown in the magnified view. + */ + getRoi: function() { + return this._zoomRegion.getROI(); + }, + + /** + * Set the "region of interest" by centering the given screen coordinate + * within the zoom region. + * @x The x-coord of the point to place at the center of the zoom region. + * @y The y-coord. + * @return Whether the shift was successful (for GS-mag, this is always + * true). + */ + shiftContentsTo: function(x, y) { + this._zoomRegion.scrollContentsTo(x, y); + return true; + }, + + /** + * moveResize + * Sets the position and size of the ZoomRegion on screen. + * @viewPort Array, [x, y, width, height], defining the position and size + * on screen to place the zoom region. + */ + moveResize: function(viewPort) { + let viewRect = { x: viewPort[0], y: viewPort[1], width: viewPort[2], height: viewPort[3] }; + this._zoomRegion.setViewPort(viewRect); + } +} + +DBus.conformExport(ShellMagnifier.prototype, MagnifierIface); +DBus.conformExport(ShellMagnifierZoomRegion.prototype, ZoomRegionIface); -- 2.39.5